package com.xaicode.auth.security;

import cn.dev33.satoken.spring.SpringMVCUtil;
import cn.dev33.satoken.stp.SaLoginConfig;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.xaicode.auth.config.properties.SystemProperties;
import com.xaicode.auth.entity.SysRole;
import com.xaicode.auth.entity.SysUser;
import com.xaicode.auth.enums.LoginType;
import com.xaicode.auth.enums.SysUserStatus;
import com.xaicode.auth.security.domain.AuthRoleVo;
import com.xaicode.auth.security.domain.LoginUser;
import com.xaicode.auth.security.scope.DataScopeType;
import com.xaicode.auth.service.ISysDeptService;
import com.xaicode.auth.service.ISysMenuService;
import com.xaicode.auth.service.ISysRoleService;
import com.xaicode.auth.service.ISysUserService;
import com.xaicode.auth.utils.DateUtils;
import com.xaicode.auth.utils.IpUtil;
import com.xaicode.auth.utils.RedisUtil;
import com.xaicode.auth.utils.RsaUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.google.common.collect.Lists;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 登陆操作接口
 * 
 * @date 2024/12/6
 */
@Slf4j
@Service
public class LoginService {

    @Autowired
    private SystemProperties systemProperties;

    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private ISysDeptService sysDeptService;
    @Autowired
    private ISysRoleService sysRoleService;
    @Autowired
    private ISysMenuService sysMenuService;

    @Autowired
    private RedisUtil redisUtil;


    public String login(String username, String password, String uuid, String code) {
        // 校验验证码
        if (systemProperties.getAuth().getCaptchaEnabled()) {
            checkCaptcha(uuid, code);
        }

        // 校验账号是否被锁定
        final String lockLoginRedisKey = AuthConst.LOCK_LOGIN_REDIS_KEY + username;
        checkLock(lockLoginRedisKey);

        // 获取账号信息
        SysUser user = getAccount(username);

        // 校验密码是否正确以及错误次数是否超限
        checkPassword(username, password, user.getPassword(), lockLoginRedisKey);

        // 校验账号状态
        checkAccountStatus(user.getStatus());

        // 组装认证后的用户实体 - 用于保存到缓存中
        LoginUser authUser = assembleAuthUser(user);

        // 进行登录
        // TODO 记录此次登录的客户端设备类型，当前默认 admin
        StpUtil.login(user.getId(),
                SaLoginConfig.setDevice(LoginType.ADMIN.getValue())
        );
        LoginUtil.setLoginUser(authUser);

        // 记录登录用户的角色
        List<SysRole> associateRoles = sysRoleService.getRolesByUserId(user.getId());
        List<AuthRoleVo> associateRoleDtos = convertSysUserListToRole(associateRoles);
        LoginUtil.setLoginUserRoles(associateRoleDtos);

        // 记录登录用户的数据权限标识
        if (user.checkIfAdminUser()) {
            // 超级管理员忽略一切判断
            LoginUtil.setLoginUserDataScopes(Lists.newArrayList(AuthConst.DATA_SCOPE_FLAG_ALL));
        } else {
            List<String> dataScopes = convertDataScopes(user.getDeptId(), associateRoles);
            LoginUtil.setLoginUserDataScopes(dataScopes);
        }

        // 记录登录用户的菜单权限
        List<String> menuPermissions = sysMenuService.getMenuPermissionByUserId(user.getId());
        LoginUtil.setLoginUserPermissions(menuPermissions);

        // 记录登录信息
        recordLoginInfo(authUser);

        // 返回token
        return StpUtil.getTokenValue();
    }

    // ----

    private void checkCaptcha(String uuid, String code) {
        final String captchaRedisKey = AuthConst.CAPTCHA_REDIS_KEY + uuid;
        if (!redisUtil.hasKey(captchaRedisKey)) {
            throw new RuntimeException("Verification code has expired");
        }
        String captcha = redisUtil.get(captchaRedisKey);
        // 验证码使用后删除
        redisUtil.delete(captchaRedisKey);
        if (!code.equals(captcha)) {
            throw new RuntimeException("Verification code error");
        }
    }

    private void checkLock(String lockLoginRedisKey) {
        Long lockExpireSeconds = redisUtil.getExpire(lockLoginRedisKey, TimeUnit.SECONDS);
        if (redisUtil.hasKey(lockLoginRedisKey)) {
            throw new RuntimeException("Account has been locked, please try again after "
                    + DateUtils.secondsToTime(lockExpireSeconds.intValue()));
        }
    }

    private SysUser getAccount(String username) {
        SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
                .eq(SysUser::getUsername, username)
                .last("limit 1")
        );
        if (Objects.isNull(user)) {
            throw new RuntimeException("Username or password error");
        }
        return user;
    }

    /**
     * 校验密码
     *
     * @param username          用户填写的用户名
     * @param password          用户填写的密码
     * @param correctPassword   数据库中的密码
     * @param lockLoginRedisKey 账号锁定的redis key
     */
    private void checkPassword(String username, String password, String correctPassword, String lockLoginRedisKey) {
        // 解密密码
        // password 传递明文时，该解密将失败，但不影响后续的匹配判定
        password = RsaUtil.decryptByPrivateKey(systemProperties.getRsa().getPrivateKey(), password);

        // 和数据库中的密码进行比对
        boolean isCorrectPassword = PasswordEncoder.matches(password, correctPassword);

        final String tryLoginRedisKey = AuthConst.TRY_LOGIN_COUNT_REDIS_KEY + username;
        if (!isCorrectPassword) {
            String countStr = redisUtil.get(tryLoginRedisKey);
            if (StrUtil.isBlank(countStr)) {
                countStr = "0";
            }
            int count = Integer.parseInt(countStr);
            if (count >= AuthConst.MAX_TRY_LOGIN_LIMIT) {
                redisUtil.setEx(lockLoginRedisKey, "1", AuthConst.LOCK_TIME, TimeUnit.SECONDS);
                throw new RuntimeException("Account has been locked, please try again after "
                        + DateUtils.secondsToTime(AuthConst.LOCK_TIME));
            }
            count++;
            redisUtil.setEx(tryLoginRedisKey, String.valueOf(count), AuthConst.TRY_LOGIN_CACHE_TIME, TimeUnit.SECONDS);
            throw new RuntimeException("Username or password error");
        }
        // 登录成功，删除尝试登录次数缓存
        redisUtil.delete(tryLoginRedisKey);
    }

    /**
     * 查看状态状态
     *
     * @param status .
     */
    private void checkAccountStatus(Integer status) {
        SysUserStatus sysUserStatus = SysUserStatus.valueOf(status);
        if (sysUserStatus != null && SysUserStatus.NORMAL != sysUserStatus) {
            switch (sysUserStatus) {
                case DISABLE:
                    throw new RuntimeException("Account has been disabled");
                case DELETED:
                    throw new RuntimeException("Account has been deleted");
                default:
                    // nothing    
            }
        }
    }

    private LoginUser assembleAuthUser(SysUser user) {
        String clientIp = IpUtil.getClientIp();
        String ipRegion = IpUtil.getIpRegion(clientIp);
        String prevIpRegion = StrUtil.isBlank(user.getLoginIp()) ? null : IpUtil.getIpRegion(user.getLoginIp());

        // 部门
        String deptName = null;
        List<String> deptNames = null;
        if (user.getDeptId() != null) {
            deptNames = sysDeptService.getParentDeptNames(user.getDeptId());
            if (CollUtil.isNotEmpty(deptNames)) {
                deptName = deptNames.get(deptNames.size() - 1);
            }
        }

        final UserAgent userAgent = UserAgent.parseUserAgentString(SpringMVCUtil.getRequest().getHeader("User-Agent"));
        String os = userAgent.getOperatingSystem().getName();
        String browser = userAgent.getBrowser().getName();

        LoginUser authUser = new LoginUser();
        authUser.setUserId(user.getId());
        authUser.setUsername(user.getUsername());
        authUser.setNickname(user.getNickname());
        authUser.setAvatar(user.getAvatar());
        authUser.setMobile(user.getMobile());
        authUser.setEmail(user.getEmail());
        authUser.setSex(user.getSex());
        authUser.setAdmin(user.getAdminFlag());
        authUser.setPrevLoginIp(user.getLoginIp());
        authUser.setPrevLoginTime(user.getLoginTime());
        authUser.setLoginIp(clientIp);
        authUser.setLoginTime(new Date());
        authUser.setCreateTime(user.getCreateTime());
        authUser.setDeptId(user.getDeptId());
        authUser.setDeptName(deptName);
        authUser.setDeptNames(deptNames);
        authUser.setBrowser(browser);
        authUser.setOs(os);
        authUser.setLoginRegion(ipRegion);
        authUser.setPrevLoginRegion(prevIpRegion);
        return authUser;
    }

    /**
     * 更新指定用户的登陆信息
     *
     * @param authUser .
     */
    private void recordLoginInfo(LoginUser authUser) {
        LambdaUpdateWrapper<SysUser> updateWrapper = new LambdaUpdateWrapper<SysUser>()
                .eq(SysUser::getId, authUser.getUserId())
                .set(SysUser::getLoginIp, authUser.getLoginIp())
                .set(SysUser::getLoginTime, LocalDateTime.now())
                .set(SysUser::getLastActiveTime, LocalDateTime.now());
        sysUserService.update(updateWrapper);
    }

    // ----

    /**
     * 转换原始 role 信息为用于认证缓存的角色信息
     *
     * @param sysRoles .
     * @return .
     */
    public List<AuthRoleVo> convertSysUserListToRole(List<SysRole> sysRoles) {
        return Optional.ofNullable(sysRoles).map(List::stream)
                .map(list -> {
                    return list.map(sysRole -> {
                        AuthRoleVo vo = new AuthRoleVo();
                        vo.setRoleId(sysRole.getId());
                        vo.setRoleName(sysRole.getRoleName());
                        vo.setRoleKey(sysRole.getRoleKey());
                        vo.setDataScope(sysRole.getDataScope());
                        return vo;
                    }).collect(Collectors.toList());
                }).orElse(new ArrayList<>());
    }

    /**
     * 基于角色构建数据权限集合
     *
     * @param userDeptId 用户关联部门id
     * @param sysRoles   .
     * @return .
     * @see DataScopeType
     */
    public List<String> convertDataScopes(Integer userDeptId, List<SysRole> sysRoles) {
        List<String> conditions = Lists.newArrayList();

        // 加入用户自身部门id
        if (Objects.nonNull(userDeptId)) {
            conditions.add(userDeptId + "");
        }

        // 兼容无角色信息处理
        if (sysRoles.isEmpty()) {
            return conditions;
        }

        for (SysRole sysRole : sysRoles) {
            Integer scope = sysRole.getDataScope();
            if (scope == null) {
                // 如果该角色没有设置数据权限，跳过
                continue;
            }
            DataScopeType scopeType = DataScopeType.parseValue(scope);
            // 如果该角色的数据权限设置不正确，跳过
            if (scopeType == null) {
                log.warn("[ConvertDataScope] 角色[{}]数据权限类型未知：roleId={} dataScope={}",
                        sysRole.getRoleName(), sysRole.getId(), scope);
                continue;
            }
            // 如果该角色的数据权限已经设置过，跳过（自定义权限除外）
            if (DataScopeType.DATA_SCOPE_CUSTOM != scopeType && conditions.contains(scope + "")) {
                continue;
            }

            switch (scopeType) {
                case DATA_SCOPE_ALL:
                    // 当角色可访问全部数据时，强制清除全部数据并返回
                    conditions.clear();
                    conditions.add(AuthConst.DATA_SCOPE_FLAG_ALL);
                    return conditions;
                case DATA_SCOPE_CUSTOM:
                    // 自定义权限，数据来自角色与部门的关系
                    List<String> customDeptIds = sysRoleService.getDeptIdsByRoleId(sysRole.getId()).stream()
                            .map(deptId -> deptId + "")
                            .collect(Collectors.toList());
                    conditions.addAll(customDeptIds);
                    break;
                case DATA_SCOPE_DEPT:
                    // 仅本部门数据权限，不含子部门，数据来自用户关联的部门信息
                    // 用户未设置部门时，默认禁止其访问相应数据
                    // 该操作已由外部处理，此处忽略
                    break;
                case DATA_SCOPE_DEPT_AND_CHILD:
                    // 本部门及子部门的数据权限
                    if (Objects.isNull(userDeptId)) {
                        break;
                    }
                    // 本部门id已由外部处理，此处忽略
                    // 获取子部门id集合
                    List<String> childDeptIds = sysDeptService.getChildrenIds(userDeptId).stream()
                            .map(deptId -> deptId + "")
                            .collect(Collectors.toList());
                    conditions.addAll(childDeptIds);
                    break;
                case DATA_SCOPE_SELF:
                    // 仅本人数据权限
                    // 该权限来自用户自身，需进行独立数据处理，即判定数据创建者是否为该用户
                    // 此处对这种情况配置一个特定标识 self，业务端需自行处理创建者过滤逻辑
                    conditions.add(AuthConst.DATA_SCOPE_FLAG_SELF);
                    break;
            }
        }

        // 去重
        conditions = conditions.stream().distinct().collect(Collectors.toList());
        return conditions;
    }

}
